#include <tinx/debug.h>
#include <tinx/device.h>
#include <tinx/fs.h>
#include <tinx/fcntl.h>
#include <tinx/memory.h>
#include <tinx/task.h>
#include <tinx/errno.h>
#include <tinx/time.h>
#include <tinx/assert.h>
#include <tinx/stdlib.h>
#include <tinx/string.h>

int sys_open(char *filename, int flags, int mode)
{
    char *name = NULL;
    task_t *task = running_task();
    inode_t *dir = named(filename, &name);
    if (!dir)
        return -ENOENT;

    inode_t *inode = NULL;

    int ret = dir->op->open(dir, name, flags, mode, &inode);
    if (ret < 0)
        goto rollback;

    iput(dir);

    file_t *file = get_file();
    file->inode = inode;
    file->mode = inode->mode;
    file->flags = flags;

    fd_t fd = task_get_file(task);
    task->files[fd] = file;
    file->offset = 0;

    if (flags & O_APPEND)
        file->offset = inode->size;

    DEBUGK("open file %s fd %d\n", filename, fd);

    return fd;

rollback:
    iput(inode);
    iput(dir);
    return ret;
}

int sys_creat(char *filename, int mode)
{
    return sys_open(filename, O_CREAT, mode);
}

int sys_close(fd_t fd)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    put_file(file);
    task_put_file(task, fd);
    return EOK;
}

extern int char_read(inode_t *inode, void *addr, int count, int offset);
extern int block_read(inode_t *inode, void *addr, int count, int offset);

int sys_read(fd_t fd, char *buf, int count)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    if ((file->flags & O_ACCMODE) == O_WRONLY)
        return -EACCES;

    inode_t *inode = file->inode;
    assert(inode);

    int len;

    if (ISCHR(inode->mode))
    {
        len = char_read(inode, buf, count, file->offset);
    }
    else if (ISBLK(inode->mode))
    {
        len = block_read(inode, buf, count, file->offset);
    }
    else
    {
        len = inode->op->read(inode, buf, count, file->offset);
    }
    if (len >= 0)
    {
        file->offset += len;
    }

    return len;
}

extern int char_write(inode_t *inode, void *addr, int count, int offset);
extern int block_write(inode_t *inode, void *addr, int count, int offset);

int sys_write(fd_t fd, char *buf, int count)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    if ((file->flags & O_ACCMODE) == O_RDONLY)
        return -EACCES;

    inode_t *inode = file->inode;
    assert(inode);

    int len;

    if (ISCHR(inode->mode))
    {
        len = char_write(inode, buf, count, file->offset);
    }
    else if (ISBLK(inode->mode))
    {
        len = block_write(inode, buf, count, file->offset);
    }
    else
    {
        len = inode->op->write(inode, buf, count, file->offset);
    }

    if (len >= 0)
    {
        file->offset += len;
    }

    return len;
}

int sys_writev(fd_t fd, iovec_t *iovec, int count)
{
    if (!iovec)
        return -EINVAL;

    int len = 0;

    for (int i = 0; i < count; i++) 
    {
        iovec_t *vec = &iovec[i];
        int ret = sys_write(fd, vec->base, vec->len);
        if (ret < 0)
        {
            return ret;
        }
        len += ret;
    }
    return len;
}

int sys_ioctl(fd_t fd, int cmd, int arg)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    inode_t *inode = file->inode;
    assert(inode);

    int ret = -ENODEV;

    if (ISCHR(inode->mode) || ISBLK(inode->mode))
    {
        assert(inode->rdev);
        ret = device_ioctl(inode->rdev, cmd, arg);
    }

    return ret;
}

// 读取一个目录项, 忽略参数 count
int sys_readdir(fd_t fd, dentry_t *entry, int count)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    if ((file->flags & O_ACCMODE) == O_WRONLY)
        return -EACCES;

    inode_t *inode = file->inode;
    assert(inode);

    if (!ISDIR(inode->mode))
        return -ENOTDIR;

    int len = inode->op->readdir(file, entry, 0);

    return len;
}

int sys_getdents(fd_t fd, dentry_t *entries, int count)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    if ((file->flags & O_ACCMODE) == O_WRONLY)
        return -EACCES;

    inode_t *inode = file->inode;
    assert(inode);

    if (!ISDIR(inode->mode))
        return -ENOTDIR;

    int len = inode->op->readdir(file, entries, count);

    return len;
}

int sys_lseek(fd_t fd, int offset, int whence)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    inode_t *inode = file->inode;
    assert(inode);

    switch (whence)
    {
    case SEEK_SET:
        file->offset = offset;
        break;
    case SEEK_CUR:
        file->offset += offset;
        break;
    case SEEK_END:
        file->offset = inode->size;
        break;
    }

    return file->offset;
}

int inode_old_stat(inode_t *inode, old_stat_t *statbuf)
{
    statbuf->size = inode->size;
    statbuf->uid = inode->uid;
    statbuf->gid = inode->gid;
    statbuf->mode = inode->mode;
    statbuf->nlinks = inode->nlinks;
    statbuf->nr = inode->nr;
    statbuf->dev = inode->dev;
    statbuf->mtime = inode->mtime;
    statbuf->ctime = inode->ctime;
    statbuf->atime = inode->atime = inode->atime = ktime();
    if (ISCHR(inode->mode) || ISBLK(inode->mode))
        statbuf->rdev = inode->rdev;
    return EOK;
}

int sys_old_fstat(fd_t fd, old_stat_t *statbuf)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    inode_t *inode = file->inode;
    assert(inode);

    int ret = inode_old_stat(inode, statbuf);

    return ret;
}

int sys_old_stat(char *pathname, old_stat_t *statbuf)
{
    inode_t *inode = namei(pathname);
    if (!inode)
        return -ENOENT;

    int ret = inode_old_stat(inode, statbuf);

    iput(inode);
    return ret;
}

int inode_stat(inode_t *inode, stat_t *statbuf)
{
    memset(statbuf, 0, sizeof(stat_t));
    statbuf->blksize = PAGE_SIZE;
    statbuf->blocks = div_round_up(inode->size, inode->block_size);
    statbuf->size = inode->size;
    statbuf->uid = inode->uid;
    statbuf->gid = inode->gid;
    statbuf->mode = inode->mode;
    statbuf->nlinks = inode->nlinks;
    statbuf->nr = inode->nr;
    statbuf->dev = inode->dev;
    statbuf->mtime = inode->mtime;
    statbuf->ctime = inode->ctime;
    statbuf->atime = inode->atime = inode->atime = ktime();
    if (ISCHR(inode->mode) || ISBLK(inode->mode))
        statbuf->rdev = inode->rdev;
    return EOK;
}

int sys_fstat(fd_t fd, stat_t *statbuf)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    inode_t *inode = file->inode;
    assert(inode);

    int ret = inode_stat(inode, statbuf);

    return ret;
}

int sys_stat(char *pathname, stat_t *statbuf)
{
    inode_t *inode = namei(pathname);
    if (!inode)
        return -ENOENT;

    int ret = inode_stat(inode, statbuf);

    iput(inode);
    return ret;
}

int sys_statx(fd_t fd, char *pathname, int flags, int mask, statx_t *statbuf)
{
    inode_t *inode = NULL;
    int ret;

    if (fd == AT_FDCWD || IS_SEPARATOR(pathname[0]))
    {
        char *name = NULL;
        inode = namei(pathname);
        if (!inode)
            return -ENOENT;
        goto makeup;
    }

    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    inode = file->inode;
    inode->count++;

makeup:
    memset(statbuf, 0, sizeof(statx_t));
    statbuf->blksize = PAGE_SIZE;
    statbuf->blocks = div_round_up(inode->size, inode->block_size);
    statbuf->size = inode->size;
    statbuf->uid = inode->uid;
    statbuf->gid = inode->gid;
    statbuf->mode = inode->mode;
    statbuf->nlinks = inode->nlinks;
    statbuf->nr = inode->nr;
    statbuf->attr = 0;
    statbuf->attrmask = 0;
    statbuf->mask = mask;
    statbuf->dev_major = inode->dev;
    statbuf->mtime.sec = inode->mtime;
    statbuf->ctime.sec = inode->ctime;
    statbuf->atime.sec = inode->atime = inode->atime = ktime();
    if (ISCHR(inode->mode) || ISBLK(inode->mode))
        statbuf->rdev_major = inode->rdev;

    iput(inode);
    return EOK;
}

int sys_mkdir(char *pathname, int mode)
{
    char *name = NULL;
    int ret;
    inode_t *dir = named(pathname, &name);
    if (!dir)
        return -ENOENT;

    if (!name[0])
    {
        ret = -ENOENT;
        goto rollback;
    }

    if (!dir->op->permission(dir, P_WRITE))
    {
        ret = -EPERM;
        goto rollback;
    }

    ret = dir->op->mkdir(dir, name, mode);

rollback:
    iput(dir);
    return ret;
}

int sys_mkdirat(fd_t fd, char *pathname, int mode)
{
    inode_t *dir = NULL;
    char *name = NULL;
    int ret;

    if (fd == AT_FDCWD || IS_SEPARATOR(pathname[0]))
    {
        char *name = NULL;
        dir = named(pathname, &name);
        if (!dir)
            return -ENOENT;
        goto makeup;
    }

    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    dir = file->inode;
    dir->count++;
    name = pathname;

makeup:
    if (!name[0])
    {
        ret = -ENOENT;
        goto rollback;
    }

    if (!dir->op->permission(dir, P_WRITE))
    {
        ret = -EPERM;
        goto rollback;
    }

    ret = dir->op->mkdir(dir, name, mode);

rollback:
    iput(dir);
    return ret;
}

int sys_rmdir(char *pathname)
{
    char *name = NULL;
    inode_t *dir = named(pathname, &name);
    if (!dir)
        return -ENOENT;

    if (!name[0])
        return -ENOENT;

    if (!dir->op->permission(dir, P_WRITE))
        return -EPERM;

    int ret = dir->op->rmdir(dir, name);

    iput(dir);
    return ret;
}

int sys_link(char *oldname, char *newname)
{
    char *oname = NULL;
    inode_t *odir = named(oldname, &oname);
    if (!odir)
        return -ENOENT;

    if (!oname[0])
        return -ENOENT;

    char *nname = NULL;
    inode_t *ndir = named(newname, &nname);
    if (!ndir)
        return -ENOENT;

    if (!nname[0])
        return -ENOENT;

    if (!ndir->op->permission(ndir, P_WRITE))
        return -EPERM;

    int ret = odir->op->link(odir, oname, ndir, nname);

    iput(odir);
    iput(ndir);

    return ret;
}

int sys_unlink(char *filename)
{
    char *name = NULL;
    inode_t *dir = named(filename, &name);
    if (!dir)
        return -ENOENT;

    if (!name[0])
        return -ENOENT;

    if (!dir->op->permission(dir, P_WRITE))
        return -EPERM;

    int ret = dir->op->unlink(dir, name);

    iput(dir);
    return ret;
}

int sys_symlink(char *oldname, char *newname)
{
    char *name;
    inode_t *dir = named(newname, &name);
    if (!dir)
        return -ENOENT;

    if (!name[0])
        return -ENOENT;

    if (!dir->op->permission(dir, P_WRITE))
        return -EPERM;

    int ret = dir->op->symlink(dir, name, oldname);

    iput(dir);
    return ret;
}

int sys_readlink(char *pathname, char *buf, int size)
{
    int ret = EOK;

    inode_t *inode = lnamei(pathname);
    if (size <= 0)
    {
        ret = -EINVAL;
        goto rollback;
    }

    if (!inode)
    {
        ret = -ENOENT;
        goto rollback;
    }

    if (!ISLNK(inode->mode))
    {
        ret = -EINVAL;
        goto rollback;
    }

    ret = inode->op->readlink(inode, buf, size);

rollback:
    iput(inode);
    return ret;
}

int sys_mknod(char *filename, int mode, dev_t dev)
{
    char *name = NULL;
    inode_t *dir = named(filename, &name);
    if (!dir)
        return -ENOENT;

    if (!name[0])
        return -ENOENT;

    if (!dir->op->permission(dir, P_WRITE))
        return -EPERM;

    device_t *device = device_get(dev);
    if (!device)
        return -ENODEV;

    int ret = dir->op->mknod(dir, name, mode, dev);

    iput(dir);
    return ret;
}

int sys_chdir(char *pathname)
{
    task_t *task = running_task();
    inode_t *inode = namei(pathname);
    if (!inode)
        return -ENOENT;

    if (!ISDIR(inode->mode))
        return -ENOTDIR;

    iput(task->ipwd);
    task->ipwd = inode;

    return EOK;
}

int sys_chroot(char *pathname)
{
    inode_t *inode = namei(pathname);
    if (!inode)
        return -ENOENT;

    if (!ISDIR(inode->mode))
        return -ENOTDIR;

    task_t *task = running_task();
    iput(task->iroot);
    task->iroot = inode;
    return EOK;
}

static fd_t dupfd(fd_t fd, fd_t arg)
{
    task_t *task = running_task();

    if (fd >= OPEN_NR)
        return -EBADF;

    if (!task->files[fd])
        return -EBADF;

    if (arg >= OPEN_NR)
        return -EINVAL;

    fd_t i = arg;

    for (; i < OPEN_NR; i++)
    {
        if (!task->files[i])
        {
            task->files[i] = task->files[fd];
            task->files[i]->count++;
            return i;
        }
    }

    return -EMFILE;
}

int sys_dup2(fd_t oldfd, fd_t newfd)
{
    sys_close(newfd);
    return dupfd(oldfd, newfd);
}

int sys_dup(fd_t fd)
{
    return dupfd(fd, 0);
}

int sys_fcntl(fd_t fd, int cmd, int arg)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    switch (cmd)
    {
    case F_DUPFD:
        return file->flags & O_CLOEXEC;
    default:
        panic("fcntl: unknow cmd %d", cmd);
    }
}

int sys_fcntl64(fd_t fd, int cmd, int arg)
{
    if (fd >= OPEN_NR)
        return -EBADF;

    task_t *task = running_task();
    file_t *file = task->files[fd];
    if (!file)
        return -EBADF;

    switch (cmd)
    {
    case F_GETFD:
        return file->flags & O_CLOEXEC;
    case F_SETFD:
        if (file->flags & O_CLOEXEC)
            file->flags &= ~O_CLOEXEC;
        else
            file->flags |= O_CLOEXEC;
        return 0;
    default:
        panic("fcntl64: unknow cmd %d", cmd);
    }
}

int sys_rename(char *oldname, char *newname)
{
    inode_t *odir = NULL;
    inode_t *ndir = NULL;
    int ret;

    odir = named(oldname, &oldname);
    if (!odir)
    {
        ret = -ENOENT;
        goto rollback;
    }

    ndir = named(newname, &newname);
    if (!ndir)
    {
        ret = -ENOTDIR;
        goto rollback;
    }

    if (!ndir->op->permission(ndir, P_WRITE))
    {
        ret = -EPERM;
        goto rollback;
    }

    if (ndir->dev != odir->dev)
    {
        ret = -EINVAL;
        goto rollback;
    }

    ret = ndir->op->rename(odir, oldname, ndir, newname);

rollback:
    iput(odir);
    iput(ndir);
    return ret;
}

int sys_mount(char *devname, char *dirname, int flags)
{
    inode_t *dir = namei(dirname);
    inode_t *devinode = NULL;
    super_t *super = NULL;

    int ret = EOK;

    if (!dir)
    {
        ret = -ENOENT;
        goto rollback;
    }

    if (!ISDIR(dir->mode))
    {
        ret = -ENOTDIR;
        goto rollback;
    }

    if (dir->mount || dir->count != 1)
    {
        ret = -EBUSY;
        goto rollback;
    }

    devinode = namei(devname);

    if (!devinode)
    {
        ret = -ENODEV;
        goto rollback;
    }

    if (!ISBLK(devinode->mode))
    {
        ret = -ENOTBLK;
        goto rollback;
    }

    dev_t dev = devinode->rdev;
    device_t *device = device_get(dev);
    assert(device->type == DEV_TYPE_BLOCK && device);

    super = read_super(dev);
    if (super->imount)
    {
        ret = -EBUSY;
        goto rollback;
    }

    super->imount = dir;
    dir->mount = super;

    iput(devinode);
    return ret;

rollback:
    put_super(super);
    iput(devinode);
    iput(dir);
    return ret;
}

// 取消挂载
// target 可能是设备根目录
// 也有可能是设备文件
int sys_umount(char *target)
{
    int ret = EOK;

    inode_t *inode = namei(target);
    super_t *super = NULL;

    if (!inode)
    {
        ret = -ENOENT;
        goto rollback;
    }

    if (!ISBLK(inode->mode) && inode->nr != 1)
    {
        ret = -ENOTBLK;
        goto rollback;
    }

    if (inode == get_root_inode())
    {
        ret = -EBUSY;
        goto rollback;
    }

    dev_t dev = ISBLK(inode->mode) ? inode->rdev : inode->mount->dev;

    super = read_super(dev); // super->count++
    if (!super->imount)
    {
        ret = -EBUSY;
        goto rollback;
    }

    // 其他 inode 未完全关闭
    if (list_size(&super->inode_list) > 1)
    {
        ret = -EBUSY;
        goto rollback;
    }

    assert(super->imount->mount);

    // 释放分区根目录
    iput(super->iroot);
    super->iroot = NULL;
    // 释放挂载 inode
    iput(super->imount);
    super->imount = NULL;

    super->count--;

rollback:
    iput(inode);
    put_super(super);
    return ret;
}